package client;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.util.Observable;
import java.util.Observer;

import javax.swing.ImageIcon;
import javax.swing.JComponent;

import utils.GameImages;
import utils.Piece;
import enums.Direction;
import enums.PlayerColor;
import enums.Revealed;
import enums.Weapon;

/**
 * The board that is drawn to the screen on which the pieces move and the game takes place.
 */
public class Board extends JComponent implements Observer {

	private static final long serialVersionUID = 1L;
	
	public  static final int CELL_SIZE = 65;
	private static final int CELL_SPACING = 3;
	public  static final int NUM_OF_ROWS = 8;
	public  static final int NUM_OF_COLS = 8;
	private static final int BORDER_WIDTH = 15;
	private static final int MOUSE_BORDER_WIDTH = 3;
	
	private static final int NUM_OF_DIRECTIONS_TO_MOVE = 4;
	
	private static final Point OFF_SCREEN_POINT = new Point (-1, -1);
	
	private static final Color COLOR_POTENTIAL_MOVE_LEGAL = Color.GREEN;
	private static final Color COLOR_POTENTIAL_MOVE_ILLEGAL = Color.RED;
	
	public static final String [] BORDER_LETTERS = 
			{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
			 "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"};
	private static final Font  LETTERS_FONT  = new Font ("SansSerif", Font.BOLD, 12);
	private static final Color LETTERS_COLOR = Color.WHITE;
	
	private static final Color DEFUALT_COLOR_CELL_FIRST  = Color.LIGHT_GRAY;
	private static final Color DEFAULT_COLOR_CELL_SECOND = Color.DARK_GRAY;
	private static final Color DEFAULT_COLOR_SPACING     = Color.BLACK;
	private static final Color DEFAULT_COLOR_MOUSE_BORDER = new Color(255, 153, 0);
	

	private FontMetrics fontMetrics = null;
	
	private boolean isAlternateBackground;
	private boolean isReversed;
	private Color cellColour1;
	private Color cellColour2;
	private Color spacingColour;
	private Color mouseBorderColour;
	private Point pressedPoint = OFF_SCREEN_POINT;
	private Point[] arrayPotentialCellsToMoveTo;
	private Color[] arrayColorsOfPotentialCellsToMoveTo;
	
	protected Model model;
	
	
	public Board(Model model)
	{
		this.model = model;

		isReversed = (model.playerColor != model.ownerColor);
		isAlternateBackground = true;

		// Determine the size of the board and set the component to that size.
		int boardWidth  = (NUM_OF_COLS * (CELL_SIZE + CELL_SPACING)) + (BORDER_WIDTH * 2) + CELL_SPACING;
		int boardHeight = (NUM_OF_ROWS * (CELL_SIZE + CELL_SPACING)) + (BORDER_WIDTH * 2) + CELL_SPACING;
		setPreferredSize (new Dimension (boardWidth, boardHeight));

		arrayPotentialCellsToMoveTo = new Point[NUM_OF_DIRECTIONS_TO_MOVE];
		arrayColorsOfPotentialCellsToMoveTo = new Color[NUM_OF_DIRECTIONS_TO_MOVE];
		
		// Set up colours to default colours
		setColours (DEFUALT_COLOR_CELL_FIRST, DEFAULT_COLOR_CELL_SECOND, DEFAULT_COLOR_SPACING, DEFAULT_COLOR_MOUSE_BORDER);

		// now render it
 		repaint ();
	}	
	
	private void setColours (Color cellColour1, Color cellColour2, Color spacingColour, Color mouseBorderColour) {
		this.cellColour1 = cellColour1;
		this.cellColour2 = cellColour2;
		this.spacingColour = spacingColour;
		this.mouseBorderColour = mouseBorderColour;
	}
	
	public void paintComponent (Graphics g) {
		// Draw the default board
		super.paintComponent (g);

		// draw the  Border
		drawBorder (g);

		// draw the background
		drawBoardBackground(g);

		drawBoardLetters (g);

		// draw each piece on the board
		drawPieces(g);
		
		if (model.getGameState() == Model.GAME_STATE_PLACING_FLAG && model.isPlayerViewer() == false) { 
			drawMouseCursorFlagPlacing(g);
		}
		else if (model.getGameState() == Model.GAME_STATE_PLACING_TRAP && model.isPlayerViewer() == false) { 
			drawMouseCursorTrapPlacing(g);
		}
		
		 //Draw the mouse boards
		if (isPointOnBoard(getPressedPoint())) {
			drawMouseBorders(g);
		}
		
		if (model.animationOn) {
			model.animation.draw(g);
		}
	}

	private void drawBoardBackground (Graphics g) {
		// Draw chess style board
		for (int x = 0; x < NUM_OF_COLS; x++) {
			// Calculate screen x coordinate
			int screenX = x * (CELL_SIZE + CELL_SPACING) + CELL_SPACING + BORDER_WIDTH;
			
			for (int y = 0; y < NUM_OF_ROWS; y++) {
				// Calculate screen y coordinate
				int screenY = y * (CELL_SIZE + CELL_SPACING) + CELL_SPACING + BORDER_WIDTH;

				// If we should alternate colors and it's time for the second color, set it
				//  Else, we should set the first color
				if (isAlternateBackground && ((x+y) % 2 == 1))
					g.setColor (this.cellColour2);
				else
					g.setColor (this.cellColour1);

				g.fillRect (screenX, screenY, CELL_SIZE, CELL_SIZE);
			}
		}
	}

	private void drawBorder (Graphics g) {
		int width = getWidth();
		int height = getHeight();
		
		// wipe the background (spacing colour)
		g.setColor (this.spacingColour);
		g.fillRect (0, 0, width, height);

		g.setColor (Color.black);
		g.drawRect (0, 0, width - 1, height - 1);
		g.drawRect (BORDER_WIDTH, BORDER_WIDTH, width - 1 - (BORDER_WIDTH * 2), height - 1 - (BORDER_WIDTH * 2));
	}

	private void drawBoardLetters(Graphics g) {
		// Create font metrics
		g.setColor (LETTERS_COLOR);
		g.setFont  (LETTERS_FONT);
		
		// Only create font metrics once (for performance)
		if (this.fontMetrics == null)
		    this.fontMetrics = g.getFontMetrics();

		int widthHalfChar = fontMetrics.charWidth('a') / 2;
		
		// Compute indent of text (using borders
		int totalCellSize = CELL_SIZE + CELL_SPACING;
		int middleX = (totalCellSize / 2) - widthHalfChar;
		int middleY = (totalCellSize + this.fontMetrics.getAscent()) / 2;

		// Draw numbers down the left
		for (int i = 0; i < NUM_OF_ROWS; i++) {
			int x = (BORDER_WIDTH / 2) - widthHalfChar + CELL_SPACING;
			int y = BORDER_WIDTH + (i * totalCellSize) + middleY;
			int number = this.isReversed ? (i + 1) : NUM_OF_COLS - i;
			if (number > 9)
			    x -= (widthHalfChar + 1);
			g.drawString (String.valueOf(number), x, y);
		}

		// Draw letters across the board
		int y = getHeight() - (BORDER_WIDTH / 2) + this.fontMetrics.getDescent();
		for (int i = 0; i < NUM_OF_COLS; i++) {
			int x = BORDER_WIDTH + (i * totalCellSize) + middleX;
			int letter = this.isReversed ? NUM_OF_ROWS - 1 - i : i;
			g.drawString (BORDER_LETTERS [letter], x, y);
		}
	}

	private void drawPieces(Graphics g) {
		
		Piece pieceTemp = new Piece(PlayerColor.RED, Direction.FRONT, Weapon.NONE, Revealed.NONE);
		ImageIcon imagePieceTemp = GameImages.getImageIcon(pieceTemp.getMap());
		int xIndent = (CELL_SIZE - imagePieceTemp.getIconWidth()) / 2;
		int yIndent = (CELL_SIZE - imagePieceTemp.getIconHeight()) / 2;

		// update each square with the  piece
		for (int y = 0; y < NUM_OF_ROWS; y++) {
			for (int x = 0; x < NUM_OF_COLS; x++) {
				Piece curPiece = model.getPiece(x, y);
				
				if (!curPiece.isEmptyPiece()) {
					if ((model.animationOn) && (model.animation.from.equals(new Point(x,y)))) {
						continue;
					}
					if ((model.trapHide) && (model.animation.to.equals(new Point(x,y)))) {
						continue;
					}
					
					String map = getMapForDrawing(curPiece);
					Point sCoords = getScreenCoords (x, y);
					g.drawImage(GameImages.getImage(map), sCoords.x + xIndent, sCoords.y + yIndent, null);
				}
			}
		}
	}
	
	/**
	 * Returns the drawing map, which is the map that doesn't necessarily represent the actual piece, but the piece we need to draw.
	 * For example - if the piece's weapon is concealed, we will change its map to contain no weapon at all, so we don't draw a weapon,
	 * but, of course, we don't want to change the actual weapon of the piece to no weapon, so we use this drawing map.
	 * 
	 * @param pieceToDraw - The piece we wish to draw
	 * @return The drawing map of the given piece pieceToDraw
	 */
	public String getMapForDrawing(Piece pieceToDraw)
	{
		String map = pieceToDraw.getMap();
		
		map = flipDirectionIfNotOwner(map);
		Piece tempPiece = new Piece(map);
		
		if (model.isPlayerViewer() == false)
		{
			if (tempPiece.color != model.playerColor && tempPiece.revealed == Revealed.NONE) {
				map = Piece.changeProperty(map, Piece.WeaponPosition, Weapon.NONE.ordinal());
			}
		}
		else
		{
			if (Boolean.parseBoolean(model.getGameSettings().getSetting(GameSettings.PROPERTY_KEY_DOES_ALLOW_VIEWERS_SEE_UNREVEALED_WEAPONS)))
			{
				if (tempPiece.weapon != Weapon.NONE && tempPiece.weapon != Weapon.FLAG && tempPiece.weapon != Weapon.TRAP)
				{
					map = Piece.changeProperty(map, Piece.RevealedPosition, Revealed.YES.ordinal());
				}
			}
			else
			{
				if (tempPiece.revealed == Revealed.NONE)
				{
					map = Piece.changeProperty(map, Piece.WeaponPosition, Weapon.NONE.ordinal());
				}
			}
			
		}
		
		tempPiece.setPiece(map);
		
		if (tempPiece.weapon != Weapon.FLAG && tempPiece.weapon != Weapon.TRAP)
		{
			int directionValue = (tempPiece.color == model.playerColor) ? Direction.BACK.ordinal() : Direction.FRONT.ordinal();
			map = Piece.changeProperty(map, Piece.DirectionPosition, directionValue);
		}
		else if (tempPiece.revealed == Revealed.NONE)
		{
			map = Piece.changeProperty(map, Piece.DirectionPosition, Direction.NONE.ordinal());
		}
		
		return map;
	}
	
	public String flipDirectionIfNotOwner(String map) {
		int i = Piece.getProperty(map, Piece.DirectionPosition);
		Direction curDirection = Direction.Convert(i);
		return model.isOwner() ? map : Piece.changeProperty(map, Piece.DirectionPosition, Direction.flipDirection(curDirection).ordinal());
	}
	
	private String flipColorIfNotOwner(String map) {
		int i = Piece.getProperty(map, Piece.PlayerColorPosition);
		PlayerColor curColor = PlayerColor.Convert(i);
		return model.isOwner() ? map : Piece.changeProperty(map, Piece.PlayerColorPosition, PlayerColor.flipColor(curColor).ordinal());
	}
	
	private void drawMouseCursorFlagPlacing(Graphics g) {

		if (model.getGameState() == Model.GAME_STATE_PLACING_FLAG) {
			Piece piece = new Piece(PlayerColor.RED, Direction.NONE, Weapon.FLAG, Revealed.NONE);
			String map = piece.getMap();
			Image image = GameImages.getImage(flipColorIfNotOwner(map));
			
			if (image != null) {
				Point mousePosition = model.getMousePosition();
				g.drawImage(image, mousePosition.x - 20, mousePosition.y - 20, null);
			}
		}
	}
	
	private void drawMouseCursorTrapPlacing(Graphics g) {

		if (model.getGameState() == Model.GAME_STATE_PLACING_TRAP) {
			Piece piece = new Piece(PlayerColor.RED, Direction.NONE, Weapon.TRAP, Revealed.NONE);
			String map = piece.getMap();
			Image image = GameImages.getImage(flipColorIfNotOwner(map));
			
			if (image != null) {
				Point mousePosition = model.getMousePosition();
				g.drawImage(image, mousePosition.x - 12, mousePosition.y - 26, null);
			}
		}
	}
	
	/**
	 * Draw borders around the selected piece's cell, and around the cells it could potentially move to.
	 * @param g - The Graphics object by which we draw
	 */
	private void drawMouseBorders(Graphics g) {
		drawMouseBorderForCell(g, pressedPoint, mouseBorderColour);
		
		for (int indexPotentialCell = 0; indexPotentialCell < NUM_OF_DIRECTIONS_TO_MOVE; ++indexPotentialCell)
		{
			drawMouseBorderForCell(g, arrayPotentialCellsToMoveTo[indexPotentialCell], arrayColorsOfPotentialCellsToMoveTo[indexPotentialCell]);
		}
	}

	private void drawMouseBorderForCell(Graphics g, Point pointCell, Color colorBorder)
	{
		if (isPointOnBoard(pointCell))
		{
			Point screenCoords = getScreenCoords (pointCell.x, pointCell.y);

			// Draw the border around the cell
			g.setColor(colorBorder);
			for (int i = 0; i < MOUSE_BORDER_WIDTH; i++)
				g.drawRect (screenCoords.x + i, screenCoords.y + i, CELL_SIZE - (i * 2) - 1, CELL_SIZE - (i * 2) - 1);
		}
	}
	
	public Point getScreenCoords (int boardX, int boardY) {
		int totalCellSize = getTotalCellSize();

		// compute screen X and Y
		int screenX = (boardX * totalCellSize) + BORDER_WIDTH + CELL_SPACING;
		int screenY = (boardY * totalCellSize) + BORDER_WIDTH + CELL_SPACING;

		if (this.isReversed) {
			screenX = ((NUM_OF_COLS - 1) * totalCellSize) -
					  (boardX * totalCellSize) + BORDER_WIDTH + CELL_SPACING;
			screenY = ((NUM_OF_ROWS - 1) * totalCellSize) -
					  (boardY * totalCellSize) + BORDER_WIDTH + CELL_SPACING;
		}

		return new Point (screenX, screenY);
	}

	public Point getBoardCoords (int screenX, int screenY) {
		int totalCellWidth = CELL_SIZE + CELL_SPACING;

		// compute new board X and Y
		if ((screenX < BORDER_WIDTH) || (screenX > (this.getWidth() - BORDER_WIDTH)) ||
			(screenY < BORDER_WIDTH) || (screenY > (this.getWidth() - BORDER_WIDTH))) {
			return OFF_SCREEN_POINT;
		}
		
		int boardX = ((screenX - BORDER_WIDTH) / totalCellWidth);
		int boardY = ((screenY - BORDER_WIDTH) / totalCellWidth);

		// note if the board is reversed then we have to allow for this also
		if (this.isReversed) {
			boardX = (NUM_OF_COLS - 1) - boardX;
			boardY = (NUM_OF_ROWS - 1) - boardY;
		}

		return new Point (boardX, boardY);
	}

	public boolean isReversed () {
		return this.isReversed;
	}

	public Point getPressedPoint() {
		return this.pressedPoint;
	}

	/**
	 * Sets pressed point in the board. It then calculates the cells to which the piece can potentially move to and whether or not
	 * it is allowed to move to each of them, according to the current state of pieces on the board.
	 *
	 * @param pressedPoint - Point on board where pressed.
	 */
	public void setPressedPoint (Point pressedPoint) {
		this.pressedPoint = pressedPoint;
		
		if (isPointOnBoard(pressedPoint) == false)
		{
			for (int indexPotentialCell = 0 ; indexPotentialCell < NUM_OF_DIRECTIONS_TO_MOVE; ++indexPotentialCell)
			{
				arrayPotentialCellsToMoveTo[indexPotentialCell] = OFF_SCREEN_POINT;
				arrayColorsOfPotentialCellsToMoveTo[indexPotentialCell] = null;
			}
		}
		else
		{
			// For the 4 directions the player can move, set it green (can move) or red (cannot move)
			int indexDirectionToMoveTo = 0;
			
			for (int deltaY = -1; deltaY <= 1; ++deltaY)
			{
				for (int deltaX = -1; deltaX <= 1; ++deltaX)
				{
					if ((Math.abs(deltaY) + Math.abs(deltaX)) <= (NUM_OF_DIRECTIONS_TO_MOVE / 4) && (deltaY != 0 || deltaX != 0))
					{
						Point currCell = new Point(pressedPoint.x + deltaX, pressedPoint.y + deltaY);
						
						if (currCell.x >= 0 && currCell.x < NUM_OF_COLS && currCell.y >= 0 && currCell.y < NUM_OF_ROWS)
						{
							arrayPotentialCellsToMoveTo[indexDirectionToMoveTo] = currCell;
							
							arrayColorsOfPotentialCellsToMoveTo[indexDirectionToMoveTo] = (model.getPiece(currCell).color != model.playerColor)
																						  ? COLOR_POTENTIAL_MOVE_LEGAL : COLOR_POTENTIAL_MOVE_ILLEGAL;
						}
						else
						{
							arrayPotentialCellsToMoveTo[indexDirectionToMoveTo] = OFF_SCREEN_POINT;
							arrayColorsOfPotentialCellsToMoveTo[indexDirectionToMoveTo] = null;
						}
						
						++indexDirectionToMoveTo;
					}
				}
			}
		}
		
		repaint();
	}

	public void setControllerAsListener (Controller controller) {
		addMouseListener (controller);
		addMouseMotionListener (controller);
		addKeyListener (controller);
	}

	/**
	 * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
	 */
	public void update (Observable observerable, Object args) {
		repaint ();
	}
	
	public int getTotalCellSize()
	{
		return Board.CELL_SIZE + Board.CELL_SPACING;
	}

	public boolean isPointOnBoard(Point point)
	{
		return (point.equals(Board.OFF_SCREEN_POINT) == false);
	}

	public void setPressedPointOffBoard()
	{
		setPressedPoint(Board.OFF_SCREEN_POINT);		
	}
	
	public static Point createOutOfBoardPoint()
	{
		return new Point(OFF_SCREEN_POINT);
	}
}
